package com.excilys.formation.project.repository.impl;

import static org.springframework.util.Assert.noNullElements;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.validation.constraints.Min;

import org.joda.time.Interval;
import org.springframework.stereotype.Repository;

import com.excilys.formation.project.entities.Operation;
import com.excilys.formation.project.entities.TypeOperation;
import com.excilys.formation.project.repository.OperationDAO;

/**
 * Implémentation simple de {@link OperationDAO}.
 * 
 * @author excilys
 * 
 */
@Repository
public class OperationDAOImpl implements OperationDAO {

	/**
	 * Entity Manager injecté par Spring.
	 */
	@PersistenceContext
	private EntityManager em;

	/**
	 * Requète principale pour les opérations.
	 */
	private StringBuilder operationQSB;
	{
		operationQSB = new StringBuilder("FROM Operation o INNER JOIN o.transaction t INNER JOIN o.compte c INNER JOIN o.typeOperation to ");
		operationQSB.append("WHERE c.numero=:numero ");
		operationQSB.append("AND o.date BETWEEN :date_debut AND :date_fin ");
	}

	@Override
	public List<Operation> getOperationsByCompteAndTimeIntervalAndType(String compteNumero, Interval interval, TypeOperation type,
			@Min(0) int offset, @Min(0) int limit) {
		return getOperationsByCompteAndTimeIntervalDependingOnType(compteNumero, interval, type, offset, limit, true);
	}

	@Override
	public List<Operation> getOperationsByCompteAndTimeIntervalExceptType(String compteNumero, Interval interval, TypeOperation type,
			@Min(0) int offset, @Min(0) int limit) {
		return getOperationsByCompteAndTimeIntervalDependingOnType(compteNumero, interval, type, offset, limit, false);
	}

	@Override
	public double getSumOperationsByCompteAndTimeIntervalAndType(String compteNumero, Interval interval, TypeOperation type) {
		Object[] args = {compteNumero, interval, type};
		noNullElements(args, "Aucun argument ne doit être null");
		StringBuilder jpqlQSB = new StringBuilder("SELECT SUM(o.montant) ");
		jpqlQSB.append(operationQSB);
		jpqlQSB.append("AND to.libelle=:type ");
		jpqlQSB.append("GROUP BY to.libelle");

		Query q = em.createQuery(jpqlQSB.toString());
		q.setParameter("numero", compteNumero);
		if (type != null) {
			q.setParameter("type", type.getLibelle());
		}
		q.setParameter("date_debut", interval.getStart());
		q.setParameter("date_fin", interval.getEnd());
		if (!q.getResultList().isEmpty()) {
			return (Double) q.getSingleResult();
		} else {
			return 0;
		}
	}

	/**
	 * Renvoie les opérations d'un compte donné dans un interval de temps à l'exception des opérations d'un certain type.
	 * 
	 * @param compteNumero
	 *            Le numéro du compte dont il faut récupérer les opérations
	 * @param interval
	 *            Interval de dates pour récupérer les opérations
	 * @param type
	 *            Type d'opérations à inclure ou exclure
	 * @param offset
	 *            numéro du premier résultat à récupérer
	 * @param limit
	 *            nombre de résultat à récupérer
	 * @param inclure
	 *            S'il faut inclure (true) ou exclure (false) le type d'opérations choisi
	 * 
	 * @return les opérations d'un compte donné.
	 */
	@SuppressWarnings("unchecked")
	private List<Operation> getOperationsByCompteAndTimeIntervalDependingOnType(String compteNumero, Interval interval, TypeOperation type,
			@Min(0) int offset, @Min(0) int limit, boolean inclure) {
		Object[] args = {compteNumero, interval};
		noNullElements(args, "compteNumero et interval ne peuvent être null");

		StringBuilder jpqlQSB = new StringBuilder("SELECT o ");
		jpqlQSB.append(operationQSB);
		if (type != null) {
			if (inclure) {
				jpqlQSB.append("AND to.libelle=:type ");
			} else {
				jpqlQSB.append("AND to.libelle!=:type ");
			}
		}

		Query q = em.createQuery(jpqlQSB.toString());
		q.setParameter("numero", compteNumero);
		if (type != null) {
			q.setParameter("type", type.getLibelle());
		}
		q.setParameter("date_debut", interval.getStart());
		q.setParameter("date_fin", interval.getEnd());
		q.setFirstResult(offset);
		q.setMaxResults(limit);
		return q.getResultList();

	}

	@Override
	public void save(Operation operation) {
		em.persist(em.merge(operation));
	}

	@Override
	public Map<String, Double> getSumEncoursByClient(long clientId) {
		Object[] args = {clientId};
		noNullElements(args, "clientId ne peut être null");

		StringBuilder jpqlQSB = new StringBuilder(
				"SELECT c.numero, SUM(o.montant) FROM Operation o INNER JOIN o.compte c ");
		jpqlQSB.append("WHERE c.client.id=:clientId ");
		jpqlQSB.append("AND o.dateEffet > o.date ");
		jpqlQSB.append("GROUP BY c.numero");

		Query q = em.createQuery(jpqlQSB.toString());
		q.setParameter("clientId", clientId);

		Map<String, Double> resultMap = new HashMap<String, Double>();

		for (Object object : q.getResultList()) {

			Object[] myArray = (Object[]) object;
			resultMap.put((String) myArray[0], (Double) myArray[1]);
		}

		return resultMap;
	}

	@Override
	public TypeOperation getTypeByID(int i) {
		return em.find(TypeOperation.class, i);
	}

	@Override
	public long getCountOperationsByCompteAndTimeIntervalAndType(String compteNumero, Interval interval, TypeOperation type) {
		Object[] args = {compteNumero, interval};
		noNullElements(args, "compteNumero et interval ne peuvent être null");
		StringBuilder jpqlQSB = new StringBuilder("SELECT COUNT(o.id) ");
		jpqlQSB.append(operationQSB);
		if (type != null) {
			jpqlQSB.append("AND to.libelle=:type ");
		}

		Query q = em.createQuery(jpqlQSB.toString());
		q.setParameter("numero", compteNumero);
		if (type != null) {
			q.setParameter("type", type.getLibelle());
		}
		q.setParameter("date_debut", interval.getStart());
		q.setParameter("date_fin", interval.getEnd());
		return (Long) q.getSingleResult();
	}

	@Override
	public long getCountOperationsByCompteAndTimeIntervalExceptType(String compteNumero, Interval interval, TypeOperation type) {
		Object[] args = {compteNumero, interval};
		noNullElements(args, "compteNumero et interval ne peuvent être null");
		StringBuilder jpqlQSB = new StringBuilder("SELECT COUNT(o.id) ");
		jpqlQSB.append(operationQSB);
		if (type != null) {
			jpqlQSB.append("AND to.libelle!=:type ");
		}

		Query q = em.createQuery(jpqlQSB.toString());
		q.setParameter("numero", compteNumero);
		if (type != null) {
			q.setParameter("type", type.getLibelle());
		}
		q.setParameter("date_debut", interval.getStart());
		q.setParameter("date_fin", interval.getEnd());
		return (Long) q.getSingleResult();
	}
}
